버퍼를 구조체로 캐스팅해 읽기 — 구조체 안의 구조체 배열과 인덱스 접근

NOTE

연속된 바이트 버퍼(Raw Data Buffer, 수신 패킷, 공유메모리)를 구조체 포인터로 캐스팅하고, 그 안에 붙어 있는 구조체 배열을 tuple->member[i].field로 읽는 방법을 정리한다. 배열 길이를 누가 아는가, 패딩 때문에 오프셋이 어떻게 달라지는가가 핵심이다.

1. 어떤 상황인가

[ 바이트 버퍼 ]  →  (struct __tuple *) 로 캐스팅  →  필드 이름으로 접근

복사·파싱 없이 포인터 캐스팅 한 번으로 읽을 수 있어서 빠르다. 단, 버퍼의 바이트 배치가 구조체 선언과 정확히 같아야 한다.

2. 구조체 선언 — 반복 요소는 “배열 멤버”로 둔다

struct __test {
    char str_val[5];
    int  length;
};
 
struct __tuple {
    char          str_val[5];
    struct __test __struct[1];   /* 뒤에 같은 요소가 여러 개 이어 붙는다 — 마지막 멤버 */
} *tuple;

[i]로 접근하려면 멤버가 배열이어야 한다

struct __test __struct;(배열 아님)로 선언하면 tuple->__struct[i]는 컴파일 에러다 — 단일 구조체에는 인덱스를 쓸 수 없다. 요소가 여러 개 붙는 구조라면 __struct[1](또는 C99 __struct[])처럼 배열 멤버로 선언한다. 이름도 struct test/struct __test처럼 어긋나지 않게 맞춘다.

마지막 멤버를 [1]/[]로 두는 것이 Struct Hack과 같은 원리다. 선언상 1개짜리지만 실제 버퍼에는 n개가 이어 붙어 있다고 약속하는 것이다.

3. 접근 방법

int i;
for (i = 0; i < n; i++) {
    printf("%.5s %d\n",
           tuple->__struct[i].str_val,      /* 요소는 "값"이므로 . 으로 접근 */
           tuple->__struct[i].length);
}
표현의미
tuple->str_val헤더 필드 (tuple이 포인터라 ->)
tuple->__struct[i]i번째 struct __test 값 (배열 요소)
tuple->__struct[i].str_val그 요소의 필드 — 요소가 값이므로 .
(tuple->__struct + i)->str_val같은 뜻. 포인터 산술로 쓴 형태 (a[i] ≡ *(a+i))
  • ->는 포인터에, .는 구조체 값에 쓴다. tuple(포인터) → ->, __struct[i](배열 요소=값) → ..
  • __struct[i]의 주소는 기준 + i * sizeof(struct __test)로 계산된다 — 그래서 요소 크기(패딩 포함)가 버퍼와 일치해야 한다.

4. 개수(n)는 누가 아는가

컴파일러는 __struct[1]의 길이를 1로만 안다. 실제 개수는 데이터에서 얻어야 한다.

방법설명
헤더에 개수 필드struct __tuple { int count; ... } 처럼 앞에 두고 i < tuple->count
버퍼 전체 크기로 계산n = (size - offsetof(struct __tuple, __struct)) / sizeof(struct __test) (수신 함수가 돌려준 전체 바이트 수 size를 쓰는 경우)
종료 요소마지막 요소에 특수값(예: length == 0)을 두고 만날 때까지 순회

개수를 확인하지 않고 i를 돌리면 버퍼 밖을 읽는다(오버리드).

5. 패딩과 오프셋 — 크기를 눈대중으로 세지 않는다

x86-64 기준(char=1, int=4바이트·4정렬) 레이아웃:

구조체필드오프셋비고
struct __teststr_val[5]05바이트
(padding)5~7int를 4배수 주소에 맞추려고 3바이트
length8
sizeof125+4=9가 아니라 12
struct __tuplestr_val[5]0
(padding)5~7요소(__test)가 4정렬이라 3바이트
__struct[0]8i번째는 8 + 12*i
sizeof20(__struct[1] 포함)

(Python ctypes로 같은 선언의 크기/오프셋을 계산해 확인한 값이다.)

  • 요소 간격은 5 + 4 = 9가 아니라 12다. 버퍼를 만드는 쪽(다른 언어/장비/프로세스)이 9바이트 간격으로 채웠다면 구조체 캐스팅이 맞지 않는다 → 그 경우는 memcpy로 필드를 하나씩 꺼내거나 __attribute__((packed))를 쓴다 (packed는 정렬 위반 접근 비용·이식성 이슈가 있다).
  • 크기·오프셋은 하드코딩하지 말고 sizeof, offsetof(<stddef.h>)로 구한다.
  • 버퍼 시작 주소가 정렬되어 있지 않으면 int 필드 접근이 느리거나 일부 CPU에서 오류가 난다 → 메모리 정렬과 aligned_alloc.

6. 문자열 필드 주의 — char str_val[5]

  • 5바이트가 모두 데이터로 차 있으면 \0이 없다. printf("%s", ...)는 뒤의 패딩/다음 필드까지 읽어 나갈 수 있다.
  • 그래서 고정폭 필드는 printf("%.5s", p)처럼 최대 길이를 지정하거나, 버퍼에 복사한 뒤 \0을 직접 붙인다.
  • 고정폭 문자열 필드를 구조체로 매핑할 때 char c[6]처럼 폭 +1바이트를 잡아 NUL 종료 문자 자리를 확보하는 관례도 있다.

7. 포인터 멤버와 헷갈리지 말 것

struct __bad {
    char           str_val[5];
    struct __test *__struct;      /* 포인터 멤버 */
};
  • 이 선언은 구조체 안에 요소가 들어있는 것이 아니라 “다른 곳의 주소”가 들어있다. 표기는 똑같이 p->__struct[i].str_val이지만 메모리 모델이 다르다.
  • 바이트 버퍼를 캐스팅해서 읽는 용도로는 쓸 수 없다 — 버퍼에는 요소 데이터가 이어 붙어 있을 뿐 주소값이 아니므로, 포인터 멤버는 버퍼의 일부 바이트를 주소로 잘못 해석해 크래시가 난다.
  • 구분법: 요소 데이터가 같은 블록에 이어 붙음 → 배열 멤버([1]/[]), 별도 할당 → 포인터 멤버(해제도 따로).

8. 정리

질문답
반복 요소를 [i]로 읽으려면?구조체의 마지막 배열 멤버로 선언
.과 -> 구분은?포인터면 ->, 배열 요소(값)면 .
개수는?헤더 필드 / 전체 크기 계산 / 종료 요소로 직접 알아내야 함
요소 간격은?sizeof(요소) (패딩 포함) — offsetof/sizeof로 계산
고정폭 문자열 출력은?%.Ns 또는 복사 후 \0 부여
포인터 멤버로 대체 가능?불가 — 버퍼 캐스팅에서는 쓸 수 없음

관련 문서